/*!
A command for copying shared code from Jiff to the `jiff-static` proc-macro.
*/

use std::{
    fmt::Write as _,
    fs, io,
    path::{Path, PathBuf},
    sync::LazyLock,
};

use anyhow::Context;
use lexopt::{Arg, Parser};
use regex_lite::Regex;

use crate::args::{self, Usage};

const USAGE: &'static str = r#"
Copy shared code from Jiff to the `jiff-static` proc-macro.

USAGE:
    jiff-cli generate shared [<jiff-dir>]

While unfortunate, this copies code inside of Jiff into the proc-macro so that
it can be used in both places. Specifically, this includes a handful of
shared types, as well as a POSIX time zone and TZif parser.

Doing things this way permits Jiff to depend on and re-export `jiff-static`.
This obviously comes at the cost of a little more compilation time, but
the amount of code copied is pretty small (<2,000 SLOC at time of writing,
2025-02-22). An alternative design would have `jiff-static` depend on
`jiff`, and `jiff` could expose (without making it part of the semver API)
the necessary parsing routines. But then Jiff couldn't re-export the macro
and users would need to specifically deal with `jiff-static` explicitly.
"#;

pub fn run(p: &mut Parser) -> anyhow::Result<()> {
    let mut config = Config::default();
    args::configure(p, USAGE, &mut [&mut config])?;

    let jiff = config.jiff();
    let jiff_dir = jiff.join("src");
    let macro_dir = jiff.join("crates/jiff-static/src");
    let dir = Path::new("shared");
    copy(&jiff_dir, &macro_dir, dir)?;

    Ok(())
}

fn copy(srcdir: &Path, dstdir: &Path, dir: &Path) -> anyhow::Result<()> {
    for result in walkdir::WalkDir::new(srcdir.join(dir)) {
        let dent = result?;
        let suffix = dent.path().strip_prefix(srcdir).unwrap();
        let dstpath = dstdir.join(suffix);
        if dent.file_type().is_dir() {
            match fs::create_dir(&dstpath) {
                Ok(()) => {}
                Err(err) if err.kind() == io::ErrorKind::AlreadyExists => {}
                Err(err) => {
                    return Err(anyhow::Error::from(err).context(format!(
                        "failed to create directory {}",
                        dstpath.display()
                    )));
                }
            }
            continue;
        }
        anyhow::ensure!(
            dent.file_type().is_file(),
            "unknown file type {:?}, unsure how to handle",
            dent.file_type(),
        );
        copy_rust_source_file(&dent.path(), &dstpath)?;
    }
    super::cargo_fmt("jiff-static")?;
    Ok(())
}

fn copy_rust_source_file(src: &Path, dst: &Path) -> anyhow::Result<()> {
    let code = fs::read_to_string(src)
        .with_context(|| format!("failed to read {}", src.display()))?;
    let code = remove_only_jiffs(&remove_cfg_alloc(&code));

    let mut out = String::new();
    writeln!(out, "// auto-generated by: jiff-cli generate shared")?;
    writeln!(out)?;
    writeln!(out, "{code}")?;
    fs::write(&dst, &out)
        .with_context(|| format!("failed to write {}", dst.display()))?;
    Ok(())
}

fn remove_only_jiffs(code: &str) -> String {
    static RE: LazyLock<Regex> = LazyLock::new(|| {
        Regex::new(
            r"(?xm)
                ^[\x20\t]*
                //\s+only-jiff-start\n
                (?s:.)+?\n
                [\x20\t]*//\s+only-jiff-end\n
            ",
        )
        .unwrap()
    });
    RE.replace_all(code, "").into_owned()
}

/// Removes all `#[cfg(feature = "alloc")]` gates.
///
/// This is because the proc-macro always runs in a context where `alloc`
/// (and `std`) are enabled.
fn remove_cfg_alloc(code: &str) -> String {
    static RE: LazyLock<Regex> = LazyLock::new(|| {
        Regex::new(r###"#\[cfg\(feature = "alloc"\)\]\n"###).unwrap()
    });
    RE.replace_all(code, "").into_owned()
}

#[derive(Debug)]
struct Config {
    jiff: Option<PathBuf>,
    verbose: bool,
}

impl Config {
    fn jiff(&self) -> &Path {
        self.jiff.as_deref().unwrap_or_else(|| Path::new("./"))
    }
}

impl Default for Config {
    fn default() -> Config {
        Config { jiff: None, verbose: false }
    }
}

impl args::Configurable for Config {
    fn configure(
        &mut self,
        _: &mut Parser,
        arg: &mut Arg,
    ) -> anyhow::Result<bool> {
        match *arg {
            Arg::Short('v') | Arg::Long("verbose") => {
                self.verbose = true;
            }
            Arg::Value(ref mut value) => {
                if self.jiff.is_none() {
                    let path = PathBuf::from(std::mem::take(value));
                    self.jiff = Some(path);
                } else {
                    return Ok(false);
                }
            }
            _ => return Ok(false),
        }
        Ok(true)
    }

    fn usage(&self) -> &[Usage] {
        const USAGES: &'static [Usage] = &[Usage::new(
            "-v, --verbose",
            "Add more output.",
            r#"
This is a generic flag that expands output beyond the "normal" amount. Which
output is added depends on the command.
"#,
        )];
        USAGES
    }
}
